/*
 * Copyright (C) 2007-2008  Hylke van der Schaaf
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, in version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package hextd.server;

import com.usoog.tdcore.UserInfo;
import hextd.Constants.settingKey;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Timer;
import java.util.TimerTask;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.usoog.tdcore.map.MapInfo;
import com.usoog.tdcore.map.MapLoader;
import com.usoog.tdcore.message.GameInfo;
import com.usoog.tdcore.message.Message;
import com.usoog.tdcore.message.MessageBuyCreep;
import com.usoog.tdcore.message.MessageFetch;
import com.usoog.tdcore.message.MessageGameCreate;
import com.usoog.tdcore.message.MessageGameEnd;
import com.usoog.tdcore.message.MessageGameJoin;
import com.usoog.tdcore.message.MessageKickUser;
import com.usoog.tdcore.message.MessageMapLoad;
import com.usoog.tdcore.message.MessagePing;
import com.usoog.tdcore.message.MessagePlayerForfeit;
import com.usoog.tdcore.message.MessagePong;
import com.usoog.tdcore.message.MessageReplay;
import com.usoog.tdcore.message.MessageSay;
import com.usoog.tdcore.message.MessageSetMode;
import com.usoog.tdcore.message.MessageSettings;
import com.usoog.tdcore.message.MessageTick;
import com.usoog.tdcore.message.MessageTowerBuy;
import com.usoog.tdcore.message.MessageTowerSell;
import com.usoog.tdcore.message.MessageTowerUpgrade;
import com.usoog.tdcore.message.MessageUnknown;
import com.usoog.tdcore.message.MessageUserInGame;
import com.usoog.tdcore.message.MessageUserList;
import java.util.List;
import java.util.Map;

public class Channel {

	/**
	 * Commands are used to encapsulate code into something that can be put in a
	 * Map
	 */
	private interface MessageCommand {

		public boolean execute(Channel c, ServerUser from, Message message);
	}
	/**
	 * The map of commands per message
	 */
	private final static Map<String, MessageCommand> messageCommands = new HashMap<String, MessageCommand>();

	/**
	 * filling the command map
	 */
	static {
		messageCommands.put(MessagePing.key, new MessageCommand() {

			public boolean execute(Channel c, ServerUser from, Message message) {
				from.sendLine(MessagePong.formatMessage());
				return false;
			}
		});
		messageCommands.put(MessageTick.key, new MessageCommand() {

			public boolean execute(Channel c, ServerUser from, Message message) {
				if (message.getSenderId() != from.getGameId()) {
					System.out.println("Channel::reveiveLine: Tick senderID not correct, got: " + message.getSenderId() + " expected " + from.getGameId());
					message.setSenderId(from.getGameId());
				}
				MessageTick mt = (MessageTick) message;
				c.lastTickByUserId.get(from.getUserId()).set(mt.getTick());
				c.sendToOtherUsers(from, message);
				c.addToLog(message, false);
				return false;
			}
		});
		MessageCommand mc = new MessageCommand() {

			public boolean execute(Channel c, ServerUser from, Message message) {
				if (message.getSenderId() != from.getGameId()) {
					System.out.println("Channel::reveiveLine: PlayerForfeit senderID not correct, got: " + message.getSenderId() + " expected " + from.getGameId());
					message.setSenderId(from.getGameId());
				}
				c.sendToOtherUsers(from, message);
				c.addToLog(message, true);
				return true;
			}
		};
		messageCommands.put(MessageBuyCreep.key, mc);
		messageCommands.put(MessageTowerBuy.key, mc);
		messageCommands.put(MessageTowerUpgrade.key, mc);
		messageCommands.put(MessageTowerSell.key, mc);
		messageCommands.put(MessageSetMode.key, mc);
		messageCommands.put(MessagePlayerForfeit.key, mc);

		messageCommands.put(MessageSettings.key, new MessageCommand() {

			public boolean execute(Channel c, ServerUser from, Message message) {
				if (message.getSenderId() != from.getUserId()) {
					message.setSenderId(from.getUserId());
				}

				MessageSettings ms = (MessageSettings) message;
				if (from.getGameId() == 0 && !c.gameInfo.started) {
					c.settings.putAll(ms.getSettings());
					for (Entry<String, String> entrySet : ms.getSettings().entrySet()) {
						settingKey key = settingKey.valueOf(entrySet.getKey());
						switch (key) {
							case openGame:
								c.gameInfo.open = Boolean.valueOf(entrySet.getValue());
								break;
							case selectedMap:
								try {
									int mapId = Integer.parseInt(entrySet.getValue());
									c.selectedMap = c.mapLoader.getMap(mapId);
									c.unknownSelectedMap = null;
								} catch (NumberFormatException e) {
									c.unknownSelectedMap = entrySet.getValue();
									System.out.println("Channel::receivedLine: Unknown map selected: " + entrySet.getValue());
								}
								break;
							case mapMD5:
								c.mapMD5 = entrySet.getValue();
								break;
							case version:
								c.version = entrySet.getValue();
								break;
						}
					}
				}
				for (Entry<String, String> entrySet : ms.getSettings().entrySet()) {
					settingKey key = settingKey.valueOf(entrySet.getKey());
					switch (key) {
						case pause:
							c.settings.put(entrySet.getKey(), entrySet.getValue());
							break;
						case ready:
							if (!c.gameInfo.started) {
								from.setReady(Boolean.parseBoolean(entrySet.getValue()));
								if (c.checkReady()) {
									try {
										if (c.unknownSelectedMap != null) {
											c.mapStartMessage = MessageMapLoad.formatMessage(c.unknownSelectedMap, c.mapMD5, c.version);
										} else {
											c.mapStartMessage = MessageMapLoad.formatMessage("" + c.selectedMap.mapId, c.mapMD5, c.version);
										}
									} catch (UnsupportedEncodingException ex) {
										Logger.getLogger(Channel.class.getName()).log(Level.SEVERE, null, ex);
									}
									c.startMapStartCountdown();
								} else {
									c.stopMapStartCountdown();
								}
							}
							break;
					}
				}
				c.sendToOtherUsers(from, message);
				return true;
			}
		});
		messageCommands.put(MessageSay.key, new MessageCommand() {

			public boolean execute(Channel c, ServerUser from, Message message) {
				if (message.getSenderId() != from.getUserId()) {
					System.out.println("Channel::reveiveLine: Say senderID not correct, got: " + message.getSenderId() + " expected " + from.getUserId());
					message.setSenderId(from.getUserId());
				}
				c.sendToOtherUsers(from, message);
				c.addToLog(message, true);
				return true;
			}
		});
		messageCommands.put(MessageGameCreate.key, new MessageCommand() {

			public boolean execute(Channel c, ServerUser from, Message message) {
				MessageGameCreate mca = (MessageGameCreate) message;
				c.server.createChannel(mca.getName(), from);
				return false;
			}
		});
		messageCommands.put(MessageGameJoin.key, new MessageCommand() {

			public boolean execute(Channel c, ServerUser from, Message message) {
				MessageGameJoin mjc = (MessageGameJoin) message;
				c.server.playerToChannel(from, mjc.getId());
				return false;
			}
		});
		messageCommands.put(MessageKickUser.key, new MessageCommand() {

			public boolean execute(Channel c, ServerUser from, Message message) {
				if (from.getGameId() == 0) {
					MessageKickUser mkp = (MessageKickUser) message;
					ServerUser target = c.usersByUserId.get(mkp.getTargetUserId());
					if (target != null) {
						c.server.playerToChannel(target, 0);
					}
				} else {
					System.out.println("Channel::reveiveLine: KickPlayer sender not 0 but " + from.getGameId());
				}
				c.addToLog(message, true);
				return false;
			}
		});
		messageCommands.put(MessageFetch.key, new MessageCommand() {

			public boolean execute(Channel c, ServerUser from, Message message) {
				MessageFetch mf = (MessageFetch) message;
				c.server.fetchFromServer(mf, from);
				return false;
			}
		});
		messageCommands.put(MessageUnknown.key, new MessageCommand() {

			public boolean execute(Channel c, ServerUser from, Message message) {
				MessageUnknown mu = (MessageUnknown) message;
				System.out.println("Channel::lineReceived: Unknown message: " + mu.getMess());
				return false;
			}
		});
		messageCommands.put(MessageGameEnd.key, new MessageCommand() {

			public boolean execute(Channel c, ServerUser from, Message message) {
				if (message.getSenderId() != from.getGameId()) {
					System.out.println("Channel::reveiveLine: PlayerForfeit senderID not correct, got: " + message.getSenderId() + " expected " + from.getGameId());
					message.setSenderId(from.getGameId());
				}
				c.sendToOtherUsers(from, message);
				c.addToLog(message, true);

				c.gameInfo.open = false;
				boolean doSend = false;
				if (!c.logSend) {
					c.logSend = true;
					doSend = c.logSend;
				}
				if (doSend) {
					MessageReplay replay = packageReplay(c, c.gameLogShort);
					c.server.addMpReplay(replay);
				}
				return true;
			}
		});
	}

	/**
	 * Method to package a log, with settings into a MessageReplay message.
	 *
	 * @param c The channel to take the settings from.
	 * @param log The log to package
	 * @return The replay message
	 */
	private static MessageReplay packageReplay(Channel c, StringBuilder log) {
		Map<String, String> options = new HashMap<String, String>();
		options.put("type", "multi");
		options.put(settingKey.publicGame.name(), c.settings.get(settingKey.publicGame.name()));
		options.put(settingKey.ladderGame.name(), c.settings.get(settingKey.ladderGame.name()));
		options.put(settingKey.selectedMap.name(), c.settings.get(settingKey.selectedMap.name()));
		options.put(settingKey.version.name(), c.settings.get(settingKey.version.name()));
		options.put(settingKey.mapMD5.name(), c.settings.get(settingKey.mapMD5.name()));
		MessageReplay replay = new MessageReplay(0, log.toString(), options);
		return replay;
	}
	private Server server;
	private List<ServerUser> users;
	private Map<Integer, ServerUser> usersByUserId;
	private Map<Integer, Integer> userIdByGameId = new HashMap<Integer, Integer>();
	private Map<Integer, Integer> gameIdByUserId = new HashMap<Integer, Integer>();
	private Map<Integer, AtomicInteger> lastTickByUserId = new HashMap<Integer, AtomicInteger>();
	private Map<String, String> settings = new HashMap<String, String>();
	private int maxGameId = 0;
	private StringBuilder gameLog = new StringBuilder();
	private StringBuilder gameLogShort = new StringBuilder();
	private boolean logSend = false;
	private MapLoader mapLoader;
	private MapInfo selectedMap;
	private String version = "unknown";
	private String mapMD5 = "unknown";
	private String unknownSelectedMap = null;
	private Timer countdownTimer;
	private TimerTask countdownTimerTask;
	private int mapStartCountdown = 5;
	private String mapStartMessage;
	private GameInfo gameInfo = new GameInfo();

	public Channel(Server server, String name, Integer id, Timer countdownTimer) {
		this.server = server;
		gameInfo.name = name;
		gameInfo.id = id;
		gameInfo.open = true;
		gameInfo.started = false;
		gameInfo.playerCount = 0;
		this.countdownTimer = countdownTimer;
		this.users = new ArrayList<ServerUser>();
		this.usersByUserId = new HashMap<Integer, ServerUser>();
		mapLoader = server.getMapLoader();
		settings.put(settingKey.pause.name(), Boolean.toString(false));
		settings.put(settingKey.ladderGame.name(), Boolean.toString(false));
		settings.put(settingKey.publicGame.name(), Boolean.toString(true));
		settings.put(settingKey.openGame.name(), Boolean.toString(true));
	}

	public synchronized void playerJoined(ServerUser player) {
		if (gameInfo.open) {
			System.out.println("Channel::playerJoined: " + player.getUserName() + "->" + this.gameInfo.name);
			try {
				sendLine(MessageSay.formatMessage(-1, "<div class='playerJoined'> &gt;&gt;&gt; " + player.getUserName() + " joined.</div>"));
			} catch (UnsupportedEncodingException ex) {
				Logger.getLogger(Channel.class.getName()).log(Level.SEVERE, null, ex);
			}

			AtomicInteger lastTick = lastTickByUserId.get(player.getUserId());
			if (lastTick == null) {
				lastTick = new AtomicInteger(0);
				lastTickByUserId.put(player.getUserId(), lastTick);
			}

			if (gameInfo.started) {
				int userId = player.getUserId();
				int gameId;
				if (gameIdByUserId.containsKey(userId)) {
					gameId = gameIdByUserId.get(userId);
					users.add(Math.min(gameId, users.size()), player);
					usersByUserId.put(player.getUserId(), player);
					player.setChannel(this);
					player.setGameId(gameId);
				} else {
					gameId = ++maxGameId;
					player.setGameId(gameId);
					users.add(player);
					usersByUserId.put(player.getUserId(), player);
					player.setChannel(this);
					userIdByGameId.put(gameId, userId);
					gameIdByUserId.put(userId, gameId);
				}
				player.sendLine(MessageUserInGame.formatMessage(gameInfo.id, gameId, gameInfo.started, lastTick.get()));
				try {
					player.sendLine(MessageSay.formatMessage(-1, "<div class='channelJoin'>Hello " + player.getUserName() + ", Welcome to " + this.gameInfo.name + "</div>"));
					player.sendLine(packageReplay(this, gameLog).getMessage());
				} catch (UnsupportedEncodingException ex) {
					Logger.getLogger(Channel.class.getName()).log(Level.SEVERE, null, ex);
				}
			} else {
				users.add(player);
				usersByUserId.put(player.getUserId(), player);
				player.setChannel(this);
				int userId = player.getUserId();
				int gameId = users.indexOf(player);
				maxGameId = users.size() - 1;
				userIdByGameId.put(gameId, userId);
				gameIdByUserId.put(userId, gameId);
				player.setGameId(gameId);
				player.sendLine(MessageUserInGame.formatMessage(gameInfo.id, gameId, gameInfo.started, lastTick.get()));
				try {
					player.sendLine(MessageSay.formatMessage(-1, "<div class='channelJoin'>Hello " + player.getUserName() + ", Welcome to " + this.gameInfo.name + "</div>"));
				} catch (UnsupportedEncodingException ex) {
					Logger.getLogger(Channel.class.getName()).log(Level.SEVERE, null, ex);
				}
				if (settings.size() > 0) {
					try {
						player.sendLine(MessageSettings.formatMessage(-1, settings));
					} catch (UnsupportedEncodingException ex) {
						Logger.getLogger(Channel.class.getName()).log(Level.SEVERE, null, ex);
					}
				}
			}
			this.sendPlayerList();
		}
		gameInfo.playerCount = users.size();
	}

	public synchronized void playerLeft(ServerUser player, String to) {
		int playerId = users.indexOf(player);
		users.remove(player);
		usersByUserId.remove(player.getUserId());
		if (users.size() > 0) {
			if (!gameInfo.started) {
				gameIdByUserId.remove(player.getUserId());
				if (playerId >= 0) {
					for (int i = playerId; i < users.size(); i++) {
						ServerUser renumberPlayer = users.get(i);
						int userId = renumberPlayer.getUserId();
						userIdByGameId.remove(i + 1);
						renumberPlayer.setGameId(i);
						userIdByGameId.put(i, userId);
						gameIdByUserId.put(userId, i);
						renumberPlayer.sendLine(MessageUserInGame.formatMessage(gameInfo.id, i, gameInfo.started, 0));
					}

					maxGameId--;
				} else {
					System.out.println("Channel::playerLeft: playerId was < 0: " + playerId);
				}

			}
			sendPlayerList();
			if (to != null) {
				try {
					sendLine(MessageSay.formatMessage(-1, "<div class='playerJoined'> &lt;&lt;&lt; " + player.getUserName() + " left to " + to + ".</div>"));
				} catch (UnsupportedEncodingException ex) {
					Logger.getLogger(Channel.class.getName()).log(Level.SEVERE, null, ex);
				}
			}
		} else {
			server.channelEmpty(this);
		}
		gameInfo.playerCount = users.size();
	}

	private void sendToOtherUsers(ServerUser from, Message message) {
		try {
			String forward = message.getMessage();
			for (ServerUser p : users) {
				if (p != from) {
					p.sendLine(forward);
				}
			}
		} catch (Exception ex) {
			Logger.getLogger(Channel.class.getName()).log(Level.SEVERE, null, ex);
		}
	}

	private void addToLog(Message message, boolean shortLogToo) {
		try {
			String forward = message.getMessage();
			if (!forward.endsWith("\n")) {
				forward += "\n";
			}
			if (shortLogToo) {
				gameLogShort.append(forward);
			}
			gameLog.append(forward);
		} catch (Exception ex) {
			Logger.getLogger(Channel.class.getName()).log(Level.SEVERE, null, ex);
		}
	}

	public synchronized void receiveMessage(ServerUser from, Message message) {
		MessageCommand command = messageCommands.get(message.getKey());
		if (command != null) {
			command.execute(this, from, message);
		} else {
			System.err.println("Channel::receiveMessage: Unhandled message type: " + message.getKey());
		}
	}

	private boolean checkReady() {
		if (unknownSelectedMap != null) {
			for (int i = 0; i < users.size(); i++) {
				ServerUser p = users.get(i);
				if (!p.isReady()) {
					return false;
				}
			}
		} else {
			if (selectedMap == null) {
				return false;
			}
			int numPlayers = selectedMap.players;
			if (users.size() < numPlayers) {
				return false;
			}
			for (int i = 0; i < numPlayers; i++) {
				ServerUser p = users.get(i);
				if (!p.isReady()) {
					return false;
				}
			}
		}
		return true;
	}

	private void stopMapStartCountdown() {
		if (countdownTimerTask != null) {
			countdownTimerTask.cancel();
			countdownTimerTask = null;
			try {
				sendLine(MessageSay.formatMessage(-1, "<div class='channel'> &gt;&gt;&gt; Game start cancelled.</div>"));
			} catch (UnsupportedEncodingException ex) {
				Logger.getLogger(Channel.class.getName()).log(Level.SEVERE, null, ex);
			}
		}
	}

	private void startMapStartCountdown() {
		if (countdownTimerTask == null) {
			mapStartCountdown = 5;
			countdownTimerTask = new TimerTask() {

				@Override
				public void run() {
					mapStartCountdownTick();
				}
			};
			countdownTimer.scheduleAtFixedRate(countdownTimerTask, 100, 1000);
		}
	}

	private void mapStartCountdownTick() {
		if (mapStartCountdown <= 0) {
			try {
				sendLine(MessageSay.formatMessage(-1, "<div class='channel'> &gt;&gt;&gt; Game starts NOW!</div>"));
			} catch (UnsupportedEncodingException ex) {
				Logger.getLogger(Channel.class.getName()).log(Level.SEVERE, null, ex);
			}
			countdownTimerTask.cancel();
			countdownTimerTask = null;

			gameLog.delete(0, gameLog.length());
			gameLogShort.delete(0, gameLogShort.length());
			gameInfo.started = true;
			logSend = false;

			sendLine(mapStartMessage);

			ArrayList<UserInfo> userInfos = new ArrayList<UserInfo>();
			for (ServerUser u : users) {
				userInfos.add(u.getUserInfo());
			}
			MessageUserList mul = new MessageUserList(userInfos);
			String logLine = mul.getMessage() + "\n";
			gameLog.append(logLine);
			gameLogShort.append(logLine);

			mapStartMessage = null;
		} else if (mapStartCountdown == 2) {
			try {
				sendLine(MessageSay.formatMessage(-1, "<div class='channel'> &gt;&gt;&gt; Game start in " + mapStartCountdown + "...</div>"));
			} catch (UnsupportedEncodingException ex) {
				Logger.getLogger(Channel.class.getName()).log(Level.SEVERE, null, ex);
			}
			gameInfo.started = true;
			mapStartCountdown--;
		} else {
			try {
				sendLine(MessageSay.formatMessage(-1, "<div class='channel'> &gt;&gt;&gt; Game start in " + mapStartCountdown + "...</div>"));
			} catch (UnsupportedEncodingException ex) {
				Logger.getLogger(Channel.class.getName()).log(Level.SEVERE, null, ex);
			}
			mapStartCountdown--;
		}
	}

	public void sendPlayerList() {
		ArrayList<UserInfo> userInfos = new ArrayList<UserInfo>();
		for (ServerUser u : users) {
			userInfos.add(u.getUserInfo());
		}
		MessageUserList mul = new MessageUserList(userInfos);
		String logLine = mul.getMessage() + "\n";
		this.sendLine(logLine);
	}

	public void sendLine(String line) {
		for (ServerUser p : this.users) {
			p.sendLine(line);
		}

	}

	public GameInfo getGameInfo() {
		return gameInfo;
	}

	public Integer getId() {
		return gameInfo.id;
	}

	public String getName() {
		return gameInfo.name;
	}

	public void setName(String name) {
		this.gameInfo.name = name;
	}

	@Override
	public String toString() {
		return gameInfo.name;
	}

	public boolean isOpen() {
		return gameInfo.open;
	}

	public boolean isGameStarted() {
		return gameInfo.started;
	}

	public int getPlayerCount() {
		return users.size();
	}
}
